Skip to main content

copp\copp\copp2\dp2/
topp2_ra.rs

1//! Reachability-analysis solver for second-order time-optimal path parameterization.
2//!
3//! # Method identity
4//! This module implements **Reachability Analysis (RA)** for
5//! **Time-Optimal Path Parameterization (TOPP2)** and shares the same state-space
6//! conventions used by **Convex-Objective Path Parameterization (COPP2)** components.
7//!
8//! # Discrete variables (local notation)
9//! On a path grid `s[0..=n]`:
10//! - `a[k]` denotes $\dot{s}_k^2$;
11//! - backward intervals are `[a_min[k], a_max[k]]` from `reach_set2`;
12//! - forward pass selects one feasible state per station, yielding the final profile `a`.
13//!
14//! # High-level pipeline
15//! 1. Build backward reachable intervals by calling `reach_set2_backward`.
16//! 2. Run forward clipping against local constraints and backward intervals.
17//! 3. Select maximal feasible `a[k]` at each stage to recover the time-optimal profile.
18
19use super::reach_set2::{ReachSet2Options, reach_set2_backward};
20use crate::copp::copp2::formulation::Topp2Problem;
21use crate::copp::{ApproxOrdering, approx_order};
22use crate::diag::{
23    CoppError, DebugVerboser, SilentVerboser, SummaryVerboser, TraceVerboser, Verboser, Verbosity,
24    format_duration_human,
25};
26use crate::math::numerical::{LpToleranceOptions, lp_1d};
27use core::f64;
28use itertools::izip;
29
30/// Solve TOPP2 with RA and return the profile $a(s)=\dot{s}^2$.
31///
32/// # Returns
33/// Returns `a` such that:
34/// - `a[0] = a_start`, `a[n] = a_final`, where `n = idx_s_final - idx_s_start`;
35/// - `a` is time-optimal under configured first-/second-order constraints.
36///
37/// The returned profile can be mapped to `t(s)` by `s_to_t_topp2`, then to sampled
38/// `s(t)` by `t_to_s_topp2`.
39///
40/// # Errors
41/// Returns `CoppError` when backward reachable-set construction fails or when
42/// forward pass cannot maintain feasibility under constraints.
43///
44/// # Contract
45/// - station interval and boundary states must be valid for the given constraints;
46/// - `options` must contain valid tolerance settings.
47pub fn topp2_ra(problem: &Topp2Problem, options: &ReachSet2Options) -> Result<Vec<f64>, CoppError> {
48    match options.verbosity {
49        Verbosity::Silent => topp2_ra_core(problem, (options, SilentVerboser)),
50        Verbosity::Summary => topp2_ra_core(problem, (options, SummaryVerboser::new())),
51        Verbosity::Debug => topp2_ra_core(problem, (options, DebugVerboser::new())),
52        Verbosity::Trace => topp2_ra_core(problem, (options, TraceVerboser::new())),
53    }
54}
55
56/// Core implementation of Reachability Analysis for TOPP2 with layered verbosity logging.
57fn topp2_ra_core(
58    problem: &Topp2Problem,
59    options_verboser: (&ReachSet2Options, impl Verboser),
60) -> Result<Vec<f64>, CoppError> {
61    let (options, mut verboser) = options_verboser;
62    if verboser.is_enabled(Verbosity::Summary) {
63        verboser.record_start_time();
64        crate::verbosity_log!(
65            Verbosity::Summary,
66            "\ntopp2_ra started: {} <= idx_s <= {}, a_start = {}, a_final = {}.",
67            problem.idx_s_interval.0,
68            problem.idx_s_interval.1,
69            problem.a_boundary.0,
70            problem.a_boundary.1,
71        );
72    }
73
74    // Step 1. Compute the backward reachable set.
75    let reach_set = reach_set2_backward(problem, options).map_err(|e| {
76        if verboser.is_enabled(Verbosity::Debug) {
77            crate::verbosity_log!(Verbosity::Debug, "{e:?}");
78        } else if verboser.is_enabled(Verbosity::Summary) {
79            crate::verbosity_log!(
80                Verbosity::Summary,
81                "topp2_ra: failed while computing backward reachable set."
82            );
83        }
84        e
85    })?;
86    let a_max = &reach_set.a_max;
87    let a_min = &reach_set.a_min;
88
89    // Step 2. Forward pass to select the maximal feasible state at each grid point.
90    if verboser.is_enabled(Verbosity::Debug) {
91        crate::verbosity_log!(Verbosity::Debug, "Forward pass started.");
92    }
93
94    let (idx_s_start, idx_s_final) = problem.idx_s_interval;
95    let n = idx_s_final - idx_s_start;
96    let mut a = vec![0.0; n + 1];
97    let mut a_prev = problem.a_boundary.0;
98    *a.first_mut().unwrap() = a_prev;
99
100    let mut a_b = Vec::<(f64, f64, f64)>::with_capacity(2 * problem.constraints.acc_rows());
101    for (k, (a_curr, &a_max_curr_, &a_min_curr_)) in
102        izip!(a.iter_mut(), a_max, a_min).enumerate().skip(1)
103    {
104        let idx_s = idx_s_start + k;
105        if verboser.is_enabled(Verbosity::Trace) {
106            crate::verbosity_log!(
107                Verbosity::Trace,
108                "\tForward pass at k = {k} (idx_s = {idx_s}): backward interval {a_min_curr_} <= a[k] <= {a_max_curr_}, a_prev = {a_prev}."
109            );
110        }
111
112        a_b.clear();
113        problem
114            .constraints
115            .fill_acc_topp2::<true>(&mut a_b, idx_s - 1);
116        // a_b.0 * a[k] + a_b.1 * a[k-1] <= a_b.2
117        let (mut a_max_curr, mut a_min_curr) = lp_1d::<true>(
118            a_b.iter().map(|&coeffs| {
119                // coeffs.0 * a_curr  + coeffs.1* a_prev <= coeffs.2
120                // coeffs.0 * a_curr <= coeffs.2 - coeffs.1 * a_prev
121                (coeffs.0, coeffs.2 - coeffs.1 * a_prev)
122            }),
123            &LpToleranceOptions::with_feas_tol(options.lp_feas_tol),
124        );
125
126        if verboser.is_enabled(Verbosity::Trace) {
127            crate::verbosity_log!(
128                Verbosity::Trace,
129                "\t\tForward LP result before clipping: {a_min_curr} <= a[k] <= {a_max_curr}."
130            );
131        }
132
133        a_max_curr = a_max_curr.min(a_max_curr_);
134        a_min_curr = a_min_curr.max(a_min_curr_);
135        if verboser.is_enabled(Verbosity::Trace) {
136            crate::verbosity_log!(
137                Verbosity::Trace,
138                "\t\tAfter clipping with backward reachable set: {a_min_curr} <= a[k] <= {a_max_curr}."
139            );
140        }
141
142        if a_max_curr.is_nan()
143            || a_min_curr.is_nan()
144            || matches!(
145                approx_order(
146                    a_max_curr,
147                    a_min_curr,
148                    options.a_cmp_abs_tol,
149                    options.a_cmp_rel_tol,
150                ),
151                ApproxOrdering::Less
152            )
153        {
154            let err = CoppError::Infeasible(
155                "topp2_ra".into(),
156                format!(
157                    "The reachable set is empty at index {} during the forward pass where a_max = {}, a_min = {}",
158                    idx_s_start + k,
159                    a_max_curr,
160                    a_min_curr
161                ),
162            );
163            if verboser.is_enabled(Verbosity::Debug) {
164                crate::verbosity_log!(Verbosity::Debug, "{err:?}");
165            } else if verboser.is_enabled(Verbosity::Summary) {
166                crate::verbosity_log!(
167                    Verbosity::Summary,
168                    "topp2_ra: the forward pass failed at index {idx_s} due to infeasibility."
169                );
170            }
171            return Err(err);
172        }
173
174        if a_max_curr.is_infinite() {
175            let err = CoppError::Unbounded(
176                "topp2_ra".into(),
177                format!(
178                    "The reachable set is unbounded at index {} during the forward pass where a_max = {}",
179                    idx_s_start + k,
180                    a_max_curr
181                ),
182            );
183            if verboser.is_enabled(Verbosity::Debug) {
184                crate::verbosity_log!(Verbosity::Debug, "{err:?}");
185            } else if verboser.is_enabled(Verbosity::Summary) {
186                crate::verbosity_log!(
187                    Verbosity::Summary,
188                    "topp2_ra: the forward pass failed at index {idx_s} due to unboundedness."
189                );
190            }
191            return Err(err);
192        }
193
194        if verboser.is_enabled(Verbosity::Debug)
195            && matches!(
196                approx_order(
197                    a_max_curr,
198                    a_min_curr,
199                    options.a_cmp_abs_tol,
200                    options.a_cmp_rel_tol,
201                ),
202                ApproxOrdering::Equal
203            )
204        {
205            crate::verbosity_log!(
206                Verbosity::Debug,
207                "The forward reachable set at k = {k} (idx_s = {idx_s}) is degenerate since a_max and a_min are approximately equal at {}.",
208                0.5 * (a_max_curr + a_min_curr)
209            );
210        }
211
212        a_prev = a_max_curr;
213        *a_curr = a_prev;
214
215        if verboser.is_enabled(Verbosity::Trace) {
216            crate::verbosity_log!(
217                Verbosity::Trace,
218                "\t\tSelected maximal feasible state: a[k] = {a_prev}."
219            );
220        }
221    }
222
223    if verboser.is_enabled(Verbosity::Summary) {
224        crate::verbosity_log!(
225            Verbosity::Summary,
226            "topp2_ra: total elapsed time = {}.\n",
227            format_duration_human(verboser.elapsed())
228        );
229    }
230
231    Ok(a)
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237    use crate::copp::InterpolationMode;
238    use crate::copp::copp2::stable::basic::{
239        Topp2ProblemBuilder, a_to_b_topp2, s_to_t_topp2, t_to_s_topp2,
240    };
241    use crate::copp::copp2::stable::reach_set2::ReachSet2OptionsBuilder;
242    use crate::path::{add_symmetric_axial_limits_for_test, lissajous_path_for_test};
243    use crate::robot::robot_core::Robot;
244    use std::time::{Duration, Instant};
245
246    #[test]
247    fn test_topp2_ra() -> Result<(), CoppError> {
248        run_test_topp2_ra_repeated(1, false)
249    }
250
251    /// Conditions: release, --include-ignored, CPU = Intel(R) Core(TM) Ultra 9 285K.
252    /// Average over 10000 experiments: tc_topp2_ra = 0.286978 ms, tc_interpolation = 0.002001 ms, t_final = 6.168578
253    #[test]
254    #[ignore = "slow"]
255    fn test_topp2_ra_robust() -> Result<(), CoppError> {
256        run_test_topp2_ra_repeated(10000, true)
257    }
258
259    fn run_test_topp2_ra_repeated(n_exp: usize, flag_print_step: bool) -> Result<(), CoppError> {
260        let mut tc_sum_ra = Duration::ZERO;
261        let mut tc_sum_interpolation = Duration::ZERO;
262        let mut t_final_sum = 0.0;
263
264        let options = ReachSet2OptionsBuilder::new()
265            .lp_feas_tol(1E-9)
266            .a_cmp_abs_tol(1E-9)
267            .a_cmp_rel_tol(1E-9)
268            .verbosity(Verbosity::Summary)
269            .build()?;
270
271        for i_exp in 0..n_exp {
272            let dim = 7;
273            let n: usize = 1000;
274            let mut robot = Robot::with_capacity(dim, n);
275
276            let mut rng = rand::rng();
277            let (s, derivs, _, _) = lissajous_path_for_test(dim, n, &mut rng).map_err(|e| {
278                CoppError::InvalidInput("lissajous_path_for_test".into(), e.to_string())
279            })?;
280            robot.with_s(&s.as_view())?;
281            robot.with_q(
282                &derivs.q.as_view(),
283                &derivs.dq.as_ref().unwrap().as_view(),
284                &derivs.ddq.as_ref().unwrap().as_view(),
285                derivs.dddq.as_ref().map(|m| m.as_view()).as_ref(),
286                0,
287            )?;
288            add_symmetric_axial_limits_for_test(&mut robot, 1.0, 1.0, None)?;
289
290            let start = Instant::now();
291            let topp2_problem = Topp2ProblemBuilder::new(&robot, (0, n - 1), (0.0, 0.0)).build()?;
292            let a_profile = topp2_ra(&topp2_problem, &options)?;
293            let tc_topp2_ra = start.elapsed();
294
295            let b_profile = a_to_b_topp2(s.as_slice(), &a_profile);
296            assert!(
297                !izip!(
298                    s.as_slice().windows(2),
299                    a_profile.windows(2),
300                    b_profile.iter()
301                )
302                .any(|(s_pair, a_pair, b)| {
303                    let ds_double = 2.0 * (s_pair[1] - s_pair[0]);
304                    let db = (a_pair[1] - a_pair[0]) / ds_double;
305                    (*b - db).abs() > 1e-3
306                }),
307                "b_profile generation failed!"
308            );
309
310            let start = Instant::now();
311            let (t_final, t_s) = s_to_t_topp2(s.as_slice(), &a_profile, 0.0);
312            assert_eq!(t_s.len(), s.ncols());
313            let tc_interpolation = start.elapsed();
314            let s_t = t_to_s_topp2(
315                s.as_slice(),
316                &a_profile,
317                &t_s,
318                InterpolationMode::UniformTimeGrid(0.0, 1E-3, true),
319            );
320
321            tc_sum_ra += tc_topp2_ra;
322            tc_sum_interpolation += tc_interpolation;
323            t_final_sum += t_final;
324
325            if flag_print_step && ((i_exp + 1) % 100 == 0) {
326                crate::verbosity_log!(
327                    Verbosity::Summary,
328                    "Exp #{}: tc_topp2_ra = {:.4} ms, tc_interpolation = {:.4} ms, t_final = {:.4} s, s_t.len() = {}",
329                    i_exp + 1,
330                    tc_topp2_ra.as_secs_f64() * 1E3,
331                    tc_interpolation.as_secs_f64() * 1E3,
332                    t_final,
333                    s_t.len()
334                );
335            }
336        }
337
338        crate::verbosity_log!(
339            Verbosity::Summary,
340            "Average over {} experiments: tc_topp2_ra = {:.6} ms, tc_interpolation = {:.6} ms, t_final = {:.6}",
341            n_exp,
342            tc_sum_ra.as_secs_f64() * 1E3 / n_exp as f64,
343            tc_sum_interpolation.as_secs_f64() * 1E3 / n_exp as f64,
344            t_final_sum / n_exp as f64
345        );
346
347        Ok(())
348    }
349}